#include "serialport.h"
#include <dirent.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <memory>

using namespace sh;

const SerialPort::OpenOptions defaultOptions = {
    SerialPort::BR115200,   //    BaudRate baudRate;
    SerialPort::DataBits8,  //    DataBits dataBits;
    SerialPort::StopBits1,  //    StopBits stopBits;
    SerialPort::ParityNone, //    Parity parity;
    false,                  // input xon
    false,                  // input xoff
    false,                  // input xany
    0,                      // c_cc vmin
    0                       // c_cc vtime
};

SerialPort::SerialPort(const std::string &path)
    : _path(path)
{
    _open_options = defaultOptions;
}

SerialPort::~SerialPort()
{
    close();
}

bool SerialPort::open()
{
    return open(_open_options);
}

bool SerialPort::open(const OpenOptions &options)
{
    if (_open_options != options)
        _open_options = options;

    _tty_fd = ::open(_path.c_str(), O_RDWR | O_NOCTTY | O_NONBLOCK);
    if (_tty_fd < 0)
    {
        _is_open = false;
        return false;
    }
    _is_open = true;

    struct termios tios;
    termiosOptions(tios, _open_options);
    tcsetattr(_tty_fd, TCSANOW, &tios);
    tcflush(_tty_fd, TCIOFLUSH);
    return true;
}

void SerialPort::termiosOptions(termios &tios, const OpenOptions &options)
{
    tcgetattr(_tty_fd, &tios);

    cfmakeraw(&tios);
    tios.c_cflag &= ~(CSIZE | CRTSCTS);
    tios.c_iflag &= ~(IXON | IXOFF | IXANY | IGNPAR);
    tios.c_lflag &= ~(ECHOK | ECHOCTL | ECHOKE);
    tios.c_oflag &= ~(OPOST | ONLCR);

    cfsetispeed(&tios, options.baudRate);
    cfsetospeed(&tios, options.baudRate);

    tios.c_iflag |= (options.xon ? IXON : 0) | (options.xoff ? IXOFF : 0) | (options.xany ? IXANY : 0);

    // data bits

    int databits[] = {CS5, CS6, CS7, CS8};
    tios.c_cflag &= ~0x30;
    tios.c_cflag |= databits[options.dataBits];

    // stop bits
    if (options.stopBits == StopBits2)
    {
        tios.c_cflag |= CSTOPB;
    }
    else
    {
        tios.c_cflag &= ~CSTOPB;
    }

    // parity
    if (options.parity == ParityNone)
    {
        tios.c_cflag &= ~PARENB;
    }
    else
    {
        tios.c_cflag |= PARENB;

        if (options.parity == PariteMark)
        {
            tios.c_cflag |= PARMRK;
        }
        else
        {
            tios.c_cflag &= ~PARMRK;
        }

        if (options.parity == ParityOdd)
        {
            tios.c_cflag |= PARODD;
        }
        else
        {
            tios.c_cflag &= ~PARODD;
        }
    }

    tios.c_cc[VMIN] = options.vmin;
    tios.c_cc[VTIME] = options.vtime;
}

bool SerialPort::isOpen() const
{
    return _is_open;
}

int SerialPort::write(const void *data, int length)
{
    return ::write(_tty_fd, data, length);
}

int SerialPort::read(void *data, int length)
{
    return ::read(_tty_fd, data, length);
}

std::string SerialPort::read(int length)
{
    std::unique_ptr<char[]> data(new(std::nothrow) char[length + 1]);
    int size = ::read(_tty_fd, data.get(), length);
    if(size > 0)
    {
        return std::string(data.get(), size);
    }
    return std::string("");
}

void SerialPort::close()
{
    ::close(_tty_fd);
    _is_open = false;
}

void SerialPort::setReadBlock(bool block)
{
    ::fcntl(_tty_fd, F_SETFL, block ? 0 : FNDELAY);
}

void SerialPort::setBaudRate(BaudRate baudRate)
{
    _open_options.baudRate = baudRate;
}

SerialPort::BaudRate SerialPort::baudRate() const
{
    return _open_options.baudRate;
}

void SerialPort::setStopBits(StopBits stopBits)
{
    _open_options.stopBits = stopBits;
}

SerialPort::StopBits SerialPort::stopBits() const
{
    return _open_options.stopBits;
}

void SerialPort::setDataBits(DataBits dataBits)
{
    _open_options.dataBits = dataBits;
}

SerialPort::DataBits SerialPort::dataBits() const
{
    return _open_options.dataBits;
}

void SerialPort::setParity(Parity parity)
{
    _open_options.parity = parity;
}

SerialPort::Parity SerialPort::parity() const
{
    return _open_options.parity;
}

void SerialPort::setReadMinSize(std::size_t vmin)
{
    _open_options.vmin = vmin;
}

std::size_t SerialPort::readMinSize() const
{
    return _open_options.vmin;
}

void SerialPort::setReadTimeout(std::size_t vtime)
{
    _open_options.vtime = vtime;
}

std::size_t SerialPort::readTimeout() const
{
    return _open_options.vtime;
}

std::vector<std::string> SerialPort::list()
{
    DIR *dir;
    struct dirent *ent;
    dir = opendir("/dev");
    std::vector<std::string> ttyList;

    while (ent = readdir(dir), ent != nullptr)
    {
        if ("tty" == std::string(ent->d_name).substr(0, 3))
        {
            ttyList.emplace_back(ent->d_name);
        }
    }

    return ttyList;
}

bool SerialPort::exists(const std::string &serialName)
{
    return access(serialName.c_str(), F_OK) == 0;
}

bool operator==(const SerialPort::OpenOptions &lhs, const SerialPort::OpenOptions &rhs)
{
    return lhs.baudRate == rhs.baudRate && lhs.dataBits == rhs.dataBits && lhs.parity == rhs.parity && lhs.stopBits == rhs.stopBits && lhs.vmin == rhs.vmin && lhs.vtime == rhs.vtime && lhs.xon == rhs.xon && lhs.xoff == rhs.xoff && lhs.xany == rhs.xany;
}

bool operator!=(const SerialPort::OpenOptions &lhs, const SerialPort::OpenOptions &rhs)
{
    return !(lhs == rhs);
}
